import Emittery from 'emittery'
import type { Web3Provider, JsonRpcSigner, Network } from '@ethersproject/providers'
import type { MetaMaskInpageProvider } from '@metamask/providers'
import type WalletConnectProvider from '@walletconnect/web3-provider'
import { Contract } from 'ethers'

export enum ProviderTypes {
  Metamask = 'Metamask',
  WalletConnect = 'WalletConnect',
}

export interface ConnectInfo {
  chainId: string
}

export interface ProviderMessage {
  type: string
  data: unknown
}

/**
 * Types of events that the Connector class will emit.
 */
export enum ConnectorEvents {
  AccountsChanged = 'AccountsChanged',
  Connect = 'Connect',
  Disconnect = 'Disconnect',
  ChainChanged = 'ChainChanged',
  NetworkChanged = 'NetworkChanged',
  Message = 'Message',
}

export interface EmittedEvents {
  [ConnectorEvents.AccountsChanged]: string[]
  [ConnectorEvents.Connect]: ConnectInfo
  [ConnectorEvents.Disconnect]: unknown
  [ConnectorEvents.ChainChanged]: string
  [ConnectorEvents.Message]: ProviderMessage
  [ConnectorEvents.NetworkChanged]: Network
}

export class Connector {
  public eventEmitter: Emittery<EmittedEvents>

  public provider?: Web3Provider

  public signer?: JsonRpcSigner

  public providerType?: ProviderTypes

  #externalProvider?: MetaMaskInpageProvider | WalletConnectProvider

  constructor() {
    this.eventEmitter = new Emittery<EmittedEvents>()
  }

  private requireProviderInitialized() {
    if (!this.provider) {
      throw new Error('Provider has not been initialized, call initProvider() first')
    }
    return this.provider
  }

  private requireExternalProviderInitialized() {
    if (!this.#externalProvider) {
      throw new Error('Provider has not been initialized, call initProvider() first')
    }
    return this.#externalProvider
  }

  private addEventListenersToProvider() {
    const provider = this.requireExternalProviderInitialized()
    provider.on('network', (newNetwork: Network) => {
      this.eventEmitter.emit(ConnectorEvents.NetworkChanged, newNetwork)
    })
    provider.on('accountsChanged', (accounts: any) => {
      this.eventEmitter.emit(ConnectorEvents.AccountsChanged, accounts)
    })
    provider.on('chainChanged', (chainId: string) => {
      this.eventEmitter.emit(ConnectorEvents.ChainChanged, chainId)
    })
    // Subscribe to session disconnection
    provider.on('disconnect', (code: number, reason: string) => {
      console.log(code, reason)
      this.eventEmitter.emit(ConnectorEvents.Disconnect, {
        code,
        reason,
      })
    })
  }

  private addEventListenersToExternalProvider() {
    const externalProvider = this.requireExternalProviderInitialized()
    externalProvider.on('connect', (connectInfo: any) => {
      this.eventEmitter.emit(ConnectorEvents.Connect, connectInfo)
    })
    externalProvider.on('disconnect', (error: any) => {
      this.eventEmitter.emit(ConnectorEvents.Disconnect, error)
    })
    externalProvider.on('accountsChanged', (accounts: any) => {
      this.eventEmitter.emit(ConnectorEvents.AccountsChanged, accounts)
    })
    externalProvider.on('chainChanged', (chainId: string) => {
      this.eventEmitter.emit(ConnectorEvents.ChainChanged, chainId)
    })
    externalProvider.on('message', (message: any) => {
      this.eventEmitter.emit(ConnectorEvents.Message, message)
    })
  }

  initProvider = async (providerType: ProviderTypes) => {
    this.providerType = providerType
    switch (providerType) {
      case ProviderTypes.Metamask: {
        const { initMetamask } = await import('./metamask')
        const { provider, signer, externalProvider } = await initMetamask()
        this.provider = provider
        this.signer = signer
        this.#externalProvider = externalProvider
        this.addEventListenersToProvider()
        break
      }
      case ProviderTypes.WalletConnect: {
        const { initWalletConnect } = await import('./wallet-connect')
        const { provider, signer, externalProvider } = await initWalletConnect()
        this.provider = provider
        this.signer = signer
        this.#externalProvider = externalProvider
        this.addEventListenersToExternalProvider()
        break
      }
    }
  }

  contractCall(address: string, abi: string[] | Object[]) {
    if (!this.provider) {
      throw new Error('Provider has not been initialized')
    }
    return new Contract(address, abi, this.provider)
  }

  contractTx(address: string, abi: string[] | Object[]) {
    if (!this.provider) {
      throw new Error('Provider has not been initialized')
    }
    return new Contract(address, abi, this.provider?.getSigner())
  }

  activate = async () => {
    switch (this.providerType) {
      case ProviderTypes.Metamask: {
        const provider = this.requireProviderInitialized()
        const accounts = await provider.send('eth_requestAccounts', [])
        this.eventEmitter.emit(ConnectorEvents.AccountsChanged, accounts)
        break
      }
      case ProviderTypes.WalletConnect: {
        const externalProvider = this.requireExternalProviderInitialized() as WalletConnectProvider
        await externalProvider.enable()
        break
      }
      default:
        break
    }
  }
}
